跳到主要内容

SpringSecurity 使用自带的 formLogin

表单登陆配置

在 WebSecurityConfig 中配置表章登录信息:

//配置安全拦截机制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin() (1)
.loginPage("/login‐view") (2)
.loginProcessingUrl("/login") (3)
.successForwardUrl("/login‐success") (4)
.permitAll();
}

(1)允许表单登录 (2)指定我们自己的登录页,spring security以重定向方式跳转到/login-view (3)指定登录处理的URL,也就是用户名、密码表单提交的目的路径 (4)指定登录成功后的跳转URL (5)我们必须允许所有用户访问我们的登录页(例如为验证的用户),这个 formLogin().permitAll() 方法允许任意用户访问基于表单登录的所有的URL。

会话控制

我们可以通过以下选项准确控制会话何时创建以及 Spring Security 如何与之交互:

通过以下配置方式对该选项进行配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}

默认情况下,Spring Security 会为每个登录成功的用户会新建一个 Session,就是 ifRequired。

若选用 never,则指示 Spring Security 对登录成功的用户不创建 Session了,但若你的应用程序在某地方新建了 session,那么 Spring Security 会用它的。

若使用 stateless,则说明 Spring Security 对登录成功的用户不会创建 Session 了,你的应用程序也不会允许新建 session。并且它会暗示不使用 cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于 REST API 及其无状态认证机制。

会话超时

可以再 servlet 容器中设置 Session 的超时时间,如下设置 Session 有效期为 3600s;

spring boot 配置文件:

server.servlet.session.timeout=3600s

session 超时之后,可以通过 Spring Security 设置跳转的路径。

http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");

expired 指 session 过期,invalidSession 指传入的 session_id 无效。

我们可以使用 httpOnly 和 secure 标签来保护我们的会话 cookie:

  • httpOnly:如果为 true,那么浏览器脚本将无法访问 cookie
  • secure:如果为 true,则 cookie 将仅通过 HTTPS 连接发送

spring boot 配置文件:

server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true

Session、Cookie 登陆认证

就是认证是否为合法用户,简单的说是登录。一般为匹对用户名和密码,即认证成功。

在 Spring Security 认证中,只要解决如下几个问题:

  • 哪个类表示用户?
  • 哪个属性表示用户名?
  • 哪个属性表示密码?
  • 怎么通过用户名取到对应的用户?
  • 密码的验证方式是什么?

所有的自定义行为都是围绕这几个问题展开的

认证的执行流程就是

  1. 它会拿到用户输入的用户名密码;
  2. 根据用户名通过 UserDetailsServiceloadUserByUsername(username) 方法获得一个用户对象;
  3. 获得一个 UserDetails 对象,获得内部的成员属性 password
  4. 通过 PasswordEncodermatchs(s1, s2) 方法对比用户的输入的密码和第3步的密码;
  5. 匹配成功;

获取用户名和密码

Spring Boot 整合 Spring Security(前后端分离时的json登录方式,解决获取不到用户名密码问题) springboot+security整合2

默认的账户名和密码的参数名分别是 usernamepassword 可以自定义账户和密码的参数名

http
.formLogin()
.usernameParameter("my_username")
.passwordParameter("my_password")

如果是自己验证用户名密码的话,Spring Security 仅仅支持传统的 form 表单方式(form-data)登录。这是一个比较大的坑点。现在都流行使用前后端分离,前端发送的是 json 格式数据。所以需要自己定制 UsernamePasswordAuthenticationFilter 这个类

获取用户名密码是在 UsernamePasswordAuthenticationFilter 这个类里面的 attemptAuthentication 方法,如下

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

再进一步可以看到获取用户名密码的方法

// 这个 passwordParameter 为 password
// 同理 usernameParameter 为 username
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}

所以如果要从 JSON 里取得密码和用户名,需要继承这个 UsernamePasswordAuthenticationFilter 类,重写 attemptAuthentication 方法(例如添加验证码之类的操作也是在这里入手)

例如这里增加一个验证码操作

public class MyUsernamePasswordAuthentication extends UsernamePasswordAuthenticationFilter{

private Logger log = LoggerFactory.getLogger(this.getClass());

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//我们可以在这里进行额外的验证,如果验证失败抛出继承AuthenticationException的自定义错误。
log.info("在这里进行验证码判断");
//只要最终的验证是账号密码形式就无需修改后续过程
return super.attemptAuthentication(request, response);
}

@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// TODO Auto-generated method stub
super.setAuthenticationManager(authenticationManager);
}
}

将自定义登录配置到 Security 中

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 跨站
.disable() // 关闭跨站检测
// 自定义鉴权过程,无需下面设置
// 验证策略
.authorizeRequests()
// 无需验证路径
.antMatchers("/public/**").permitAll()
.antMatchers("/user/**").permitAll()
// 放行登录
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser") // 拥有权限才可访问
// 拥有任一权限即可访问
.antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")
// 角色类似,hasRole(),hasAnyRole()
.anyRequest().authenticated()
.and()
// 自定义异常处理
.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint) // 未登录处理
.accessDeniedHandler(myAccessDeniedHandler)//权限不足处理
.and()
// 加入自定义登录校验
.addFilterBefore(myUsernamePasswordAuthentication(),UsernamePasswordAuthenticationFilter.class)
// 默认放在内存中
.rememberMe()
.rememberMeServices(rememberMeServices())
.key("INTERNAL_SECRET_KEY")
// 重写 usernamepasswordauthenticationFilter 后,下面的formLogin()设置将失效,需要手动设置到个性化过滤器中
// .and()
// .formLogin()
// .loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转面
// .loginProcessingUrl("/public/login")//登录api
// .successForwardUrl("/success")
// .failureForwardUrl("/failed")
// .usernameParameter("id")
// .passwordParameter("password")
// .failureHandler(myAuthFailedHandle) //登录失败处理
// .successHandler(myAuthSuccessHandle)//登录成功处理
// .usernameParameter("id")
.and()
.logout()//自定义登出
.logoutUrl("/public/logout")
.logoutSuccessUrl("public/logoutSuccess")
.logoutSuccessHandler(myLogoutSuccessHandle);
}

// 然后再编写Bean,代码如下:
@Bean
public MyUsernamePasswordAuthentication myUsernamePasswordAuthentication(){
MyUsernamePasswordAuthentication myUsernamePasswordAuthentication = new MyUsernamePasswordAuthentication();
myUsernamePasswordAuthentication.setAuthenticationFailureHandler(myAuthFailedHandle); //设置登录失败处理类
myUsernamePasswordAuthentication.setAuthenticationSuccessHandler(myAuthSuccessHandle);//设置登录成功处理类
myUsernamePasswordAuthentication.setFilterProcessesUrl("/public/login");
myUsernamePasswordAuthentication.setRememberMeServices(rememberMeServices()); //设置记住我
myUsernamePasswordAuthentication.setUsernameParameter("id");
myUsernamePasswordAuthentication.setPasswordParameter("password");
return myUsernamePasswordAuthentication;
}

UserDetails

参考资料 Spring Security自定义用户认证

这个接口就是下面的 UserDetailsService 的返回值,虽然 Spring Security 自带的实现类 org.springframework.security.core.userdetails.User 已经够强大了,但是还有有必要去了解这个接口,以便后续自定义

该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:

public interface UserDetails extends Serializable {

Collection<? extends GrantedAuthority> getAuthorities();

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}
  • getAuthorities 获取用户包含的权限,返回权限集合,权限是一个继承了 GrantedAuthority 的对象;
  • getPasswordgetUsername 用于获取密码和用户名;
  • isAccountNonExpired 方法返回 boolean 类型,用于判断账户是否未过期,未过期返回 true 反之返回 false
  • isAccountNonLocked 方法用于判断账户是否未锁定;
  • isCredentialsNonExpired 用于判断用户凭证是否没过期,即密码是否未过期;
  • isEnabled 方法用于判断用户是否可用。

UserDetailsService

怎么通过用户名取到对应的用户?

只需要去实现这个 UserDetailsService 接口,里面有个 loadUserByUsername 方法就是用来找到用户的

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在这个 loadUserByUsername(String username) 里面实现登陆逻辑,如果未找到用户可以抛出一个 UsernameNotFoundException 异常,返回值是一个 UserDetails 接口的实现类,默认会使用 Spring Security 定义的 User

实现 UserDetailsService 接口例

@Service
public class UserDetailServiceImpl implements UserDetailsService {

// 懒得连接数据库,这里直接使用 Map 替代
private static final Map<String, String> dates;

static {
dates = new HashMap<>();
// username, password
dates.put("admin", "admin");
}

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String rawPassword = dates.get(username);
// 这里模拟去数据库查询
if (rawPassword == null) {
throw new UsernameNotFoundException("用户名不存在");
}

// 匹配成功则用其生成加密后的密文传入 UserDetails
String password = passwordEncoder.encode(rawPassword);
// 这个 commaSeparatedStringToAuthorityList 就是把输入的字符串根据逗号分割成 List<GrantedAuthority>
return new User(username, password, AuthorityUtils
.commaSeparatedStringToAuthorityList("admin,normal"));
}
}

这里 User 对象返回值的第三个参数实际上就是权限列表 Collection<? extends GrantedAuthority> authorities

这里通过 AuthorityUtils.commaSeparatedStringToAuthorityList() 这个工具类将权限转换成 GrantedAuthority 集合(使用 , 分割不同权限)

比对权限是否存在的方式

@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 获取主体
Object principal = authentication.getPrincipal();
log.info(request.getRequestURI());
// 判断主体是否属于 UserDetails
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
// 获取权限列表
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 判断请求的 URI 是否在权限里
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}

PasswordEncoder

loadUserByUsername 这个方法可以看到只有一个 username 参数,那密码在哪里比较呢? 密码的比较使用的是 PasswordEncoder;它也是一个接口

public interface PasswordEncoder {
// 加密密码
String encode(CharSequence rawPassword);
// 对密码进行比对
boolean matches(CharSequence rawPassword, String encodedPassword);
// 对已经加密的密码再次加密
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

不过一般使用自带的实现类 BCryptPasswordEncoder 使用例

@Test
void testPasswordEncoder() {
PasswordEncoder pw = new BCryptPasswordEncoder();
// 加密密码
// 注意 这个 BCrypt加密算法每次加密得到的密文都是不一样的,所以就算是一样的密码两次加密都不会相同
String encode = pw.encode("12345678");
log.info(encode);
// 比对密码
boolean matches = pw.matches("12345678", encode);
log.info(String.valueOf(matches));
}

/** 输出如下
* $2a$10$28sxxWLV85qKIIGK4mR9YuM/JjBGotUnaX8WROHjDV1IcLsmXIhOG
* true
*/

登陆成功处理器

就是去实现 AuthenticationSuccessHandler 接口

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
RespBean ok = RespBean.ok("登录成功!", authentication.getPrincipal());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(ok));
out.flush();
out.close();
}
}

然后再在配置里使用自定义的处理器就行了

http
...
.successHandler(new MyAuthenticationSuccessHandler())

登陆失败处理器

去实现 AuthenticationFailureHandler 接口

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
RespBean error = RespBean.error("登录失败");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(error));
out.flush();
out.close();
}
}

然后再在配置里使用自定义的处理器

http
...
.failureHandler(new MyAuthenticationFailureHandler())

自定义 403 处理

去实现 AccessDeniedHandler 接口

public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
RespBean error = RespBean.error("权限不足,访问失败");
response.setStatus(403);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(error));
out.flush();
out.close();
}
}

除了上面那种高度自定义的写法还可以像这样直接调用方法

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.sendError(403, "权限不足,访问失败");
}

注册这个 Handle

http
.exceptionHandling()
// 这个 Http403ForbiddenEntryPoint 是自带的实现类
.authenticationEntryPoint(new Http403ForbiddenEntryPoint()) // 未登陆的请求处理
.accessDeniedHandler(new MyAccessDeniedHandler()) // 未授权的请求处理

如何判断用户状态的?

用户登陆后 Spring Security 是怎么知道用户已经登陆过了呢?答案就是 Cookie 和 Session,用户登陆后服务端会返回一个 Cookie

JSESSIONID=41AEFF280BB9BF67CC23E9B9C740B075;

因为每次请求都会带上 Cookie,所以 Spring Security 能基于此完成对用户权限的鉴别

注意:如果使用的是 Ajax 跨域请求,需要配置一下,否则默认是不携带 Cookie 的

这里只讲 axios 的设置,后端的设置参看跨域请求那一篇文章

// 因为默认 Spring Security 是通过 Cookie 和 Session 来验证身份的,所以需要配置携带 Cookie
axios.interceptors.request.use(config => {
config.withCredentials = true;
return config;
});

退出

Spring security 默认实现了 logout 退出,访问 /logout,果然不出所料,退出功能 Spring 也替我们做好了。

点击 “Log Out” 退出 成功。

退出后访问其它 url 判断是否成功退出。

这里也可以自定义退出成功的页面:

在 WebSecurityConfig 的 protected void configure(HttpSecurity http) 中配置:

.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login‐view?logout");

当退出操作出发时,将发生:

  • 使 HTTP Session 无效
  • 清除 SecurityContextHolder
  • 跳转到 /login-view?logout

但是,类似于配置登录功能,咱们可以进一步自定义退出功能:

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//...
.and()
.logout() (1)
.logoutUrl("/logout") (2)
.logoutSuccessUrl("/login‐view?logout") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.addLogoutHandler(logoutHandler) (5)
.invalidateHttpSession(true); (6)
}

(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用 (2)设置触发退出操作的 URL (默认是 /logout ). (3)退出之后跳转的 URL。默认是 /login?logout。 (4)定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl() 的设置会被忽略。 (5)添加一个 LogoutHandler ,用于实现用户退出时的清理工作默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 。 (6)指定是否在退出时让 HttpSession 无效。 默认设置为 true。

注意:如果让 logout 在 GET 请求下生效,必须关闭防止 CSRF 攻击 csrf().disable()。如果开启了 CSRF,必须使用 post 方式请求 /logout

logoutHandler

一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。

下面是 Spring Security 提供的一些实现:

  • PersistentTokenBasedRememberMeServices 基于持久化 token 的 RememberMe 功能的相关清理
  • TokenBasedRememberMeService 基于 token 的 RememberMe 功能的相关清理
  • CookieClearingLogoutHandler 退出时 Cookie 的相关清理
  • CsrfLogoutHandler 负责在退出时移除 csrfToken
  • SecurityContextLogoutHandler 退出时 SecurityContext 的相关清理

链式 API 提供了调用相应的 LogoutHandler 实现的快捷方式,比如 deleteCookies()

自定义登陆页

默认 Spring Security 有一个自带的登陆界面,其账户名是 user 密码会在控制台打印出来,用户访问 /login 路径会跳转到这个登陆页,虽然集成了登陆页挺好的,但是一般都是需要各种客制化,所以还是需要掌握如何自定义登陆

前端分离登陆

参考资料 Spring Security登录使用JSON格式数据 参考资料 spring security简单教程以及实现完全前后端分离

现在大部分前后端分离的 Web 程序,尤其是前端普遍使用 Ajax 请求时,Spring Security 自带的登录系统就有一些不满足需求了。

因为 Spring Security 有自己默认的登录页,自己默认的登录控制器。而登录成功或失败,都会返回一个 302 跳转。登录成功跳转到主页,失败跳转到登录页。如果未认证直接访问也会跳转到登录页。但是如果前端使用 Ajax 请求,Ajax 是无法处理 302 请求的。前后端分离 Web 中,规范是使用 Json 交互。我们希望登录成功或者失败都会返回一个 Json。

注:登录接口和登录页面的区别,登录页面就是浏览器展示出来的页面;登录接口则是提交登录数据的地方,就是登录页面里边的 form 表单的 action 属性对应的值。

在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login,也就是说,默认会存在如下两个请求:

GET http://localhost:8080/login
POST http://localhost:8080/login
  • loginProcessingUrl:这个表示配置处理登录请求的接口地址,例如你是表单登录,那么 form 表单中 action 的值就是这里填的值。
  • loginPage:这个表示登录页的地址,例如当你访问一个需要登录后才能访问的资源时,系统就会自动给你通过重定向跳转到这个页面上来。(但是这个是前后端不分家时才使用的东西)

可以自定义 AuthenticationSuccessHandlerAuthenticationFailureHandlerAccessDeniedHandler 来返回不同的 JSON

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Autowired
private UserDetailServiceImpl userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义的 UserDetailService
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 验证策略
.authorizeRequests()
// 放行登录
.antMatchers(HttpMethod.POST,"/doLogin").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
// 设置登陆成功后的处理(这个 MyAuthenticationSuccessHandler 是之前自定义的处理器,下面的同理)
.successHandler(new MyAuthenticationSuccessHandler())
// 设置登陆失败后的处理
.failureHandler(new MyAuthenticationFailureHandler())
.and()
.csrf()// 要关掉这个 csrf
.disable()
.exceptionHandling()
.authenticationEntryPoint(new Http403ForbiddenEntryPoint()) // 未登陆直接请求资源的处理(这里设置为返回 403)
.accessDeniedHandler(new MyAccessDeniedHandler())
.and().cors();

}
}

注意这里的坑点!!csrf().disable() 这里的 csrf 一定要关掉,否则会一直显示 “权限不足,访问失败”。在 Security 的默认拦截器里,默认会开启 CSRF 处理,判断请求是否携带了 token,如果没有就拒绝访问。

不要将 CORS(跨站资源共享) 和 CSRF(跨站请求伪造)弄混

  • CORS(跨站资源共享) 是局部打破同源策略的限制,使在一定规则下 HTTP 请求可以突破浏览器限制,实现跨站访问。
  • CSRF 是一种网络攻击方式,也可以说是一种安全漏洞,这种安全漏洞在 web 开发中广泛存在。

退出登陆

http
.logout()
// 默认是 /logout
.logoutUrl("/doLogout")
//退出成功,返回json
.logoutSuccessHandler((request,response,authentication) -> {
RespBean ok = RespBean.ok("退出成功!", authentication.getPrincipal());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(ok));
out.flush();
out.close();
}).permitAll();

配置测试环境

使用 Vue 搭建一个测试环境

<body>
<div id="app">
<div>
<span>username:</span>
<input type="text" v-model='username'>
</div>
<div>
<span>password:</span>
<input type="text" v-model='password'>
</div>
<button @click='sub'>提交</button>
<br/>
<br/>
<!-- 登陆成功后再访问这个 api -->
<button @click="sayHello()">测试访问需要权限的 api</button>
<br/>
<br/>
<button @click="logout()">退出登陆</button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.bootcss.com/qs/6.5.1/qs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const qs = Qs // 引入QS库把请求参数转成表单形式
let app = new Vue({
el: '#app',
created() {
axios.defaults.baseURL = 'http://127.0.0.1:8080/'
// 因为默认 Spring Security 是通过 Cookie 和 Session 来验证身份的,所以需要配置携带 Cookie
axios.interceptors.request.use(config => {
config.withCredentials = true;
return config;
});
},
data: {
password: 'admin',
username: 'admin'
},
methods: {
sub() {
axios.post('/doLogin', qs.stringify({
username: this.username,
password: this.password
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then((result) => {
console.log(result.data);
}).catch((err) => {
console.log(err);
});
},
sayHello() {
axios.get('/hello')
.then((result) => {
console.log(result.data);
}).catch((err) => {
console.log(err);
});
},
logout() {
axios.post('/doLogout')
.then((result) => {
console.log(result.data);
}).catch((err) => {
console.log(err);
});
}
},
})
</script>
</body>